package ar.edu.itba.pod.legajo46470.simulation;

import java.io.IOException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Random;

import org.apache.log4j.Logger;
import org.joda.time.Duration;

import ar.edu.itba.balance.api.AgentsBalancer;
import ar.edu.itba.balance.api.AgentsTransfer;
import ar.edu.itba.balance.api.NodeAgent;
import ar.edu.itba.balance.api.NotCoordinatorException;
import ar.edu.itba.event.RemoteEventDispatcher;
import ar.edu.itba.node.Node;
import ar.edu.itba.node.NodeInformation;
import ar.edu.itba.node.api.ClusterAdministration;
import ar.edu.itba.node.api.NodeStatistics;
import ar.edu.itba.node.api.StatisticReports;
import ar.edu.itba.pod.agent.market.Consumer;
import ar.edu.itba.pod.agent.market.Market;
import ar.edu.itba.pod.agent.market.Producer;
import ar.edu.itba.pod.agent.market.Resource;
import ar.edu.itba.pod.agent.runner.Agent;
import ar.edu.itba.pod.legajo46470.balance.AgentsBalancerImpl;
import ar.edu.itba.pod.legajo46470.balance.AgentsTransferImpl;
import ar.edu.itba.pod.legajo46470.cluster.ClusterAdministrationImpl;
import ar.edu.itba.pod.legajo46470.cluster.node.ClusterNode;
import ar.edu.itba.pod.legajo46470.cluster.ui.ClusterAdministrationUI;
import ar.edu.itba.pod.legajo46470.cluster.ui.UIAgentChooser;
import ar.edu.itba.pod.legajo46470.communication.ClusterCommunication;
import ar.edu.itba.pod.legajo46470.event.DistributedEventDispatcher;
import ar.edu.itba.pod.legajo46470.event.RemoteEventDispatcherImpl;
import ar.edu.itba.pod.legajo46470.statistics.DistributedStatistics;
import ar.edu.itba.pod.multithread.EventDispatcher;
import ar.edu.itba.pod.thread.CleanableThread;
import ar.edu.itba.pod.time.TimeMapper;
import ar.edu.itba.pod.time.TimeMappers;

public class DistributedSimulationApp implements Runnable {
	private final static Logger LOGGER = Logger.getLogger(DistributedSimulationApp.class);
	private ClusterNode clusterNode;
	private ClusterAdministrationUI cui;
	private EventDispatcher localDispatcher ;
	private DistributedSimulation distributedSimulation;
	private Registry registry;
	private ClusterCommunication<StatisticReports> clusterComm;
	private ClusterCommunication<AgentsBalancer> clusterCommBalance;
	private Random random;
	
	public static void main(String[] args) {
		new DistributedSimulationApp().run();
	}

	@Override
	public void run() {
		TimeMapper timeMapper = TimeMappers.oneSecondEach(Duration.standardHours(6));
		random = new Random();
		
		boolean exit = false, configure, firstTime;
		
		configure = firstTime = true;
		
		System.out.println("Starting ...");
		// Starts ConsoleUI and binds ClusterAdministration to local rmi registry
		try {
			while(!exit) {
				if (configure) {
					
					initClusterUI();
					
					if (firstTime) {
						try {
							initStub();
						} catch (IllegalStateException e) {
							break;
						}
						firstTime = false;
					}
					// Creates the distributed simulation and store it on the Node
					distributedSimulation = new DistributedSimulation(timeMapper, clusterNode, localDispatcher);
					StatisticReports statisticsReports = new DistributedStatistics(distributedSimulation);
					clusterNode.setSimulation(distributedSimulation);
					clusterNode.setStatisticsReports(statisticsReports);
					prepareStatistics(statisticsReports);
					
					
					// Creates a group or connects to an existing one!
					if (cui.createOptionSelected()) {
						clusterNode.getClusterAdministration().createGroup();
					} else {
						clusterNode.getClusterAdministration().connectToGroup(cui.getRemoteHost().getHostAddress(), cui.getRemotePort());
						clusterNode.startElection();
					}
					
					configure = false;
					clusterComm = new ClusterCommunication<StatisticReports>(clusterNode, Node.STATISTIC_REPORTS);
					clusterCommBalance = new ClusterCommunication<AgentsBalancer>(clusterNode, Node.AGENTS_BALANCER);
					prepareLocalAgents();
				}
				
				switch(cui.menu()) {
					case START:
						// Starts simulation
						setupSimulation().start();
						break;
					case GETGROUPID:
						System.out.println("GroupId: " + clusterNode.getClusterAdministration().getGroupId());
						break;
					case GETAGENTNUM:
						System.out.println("Agents Number: " + distributedSimulation.agentsRunning());
						break;
					case CONNECTEDNODES:
						System.out.println("Neighbours: " + clusterNode.getClusterAdministration().connectedNodes().toString());
						break;
					case ADDAGENT:
						addAgentToSimulation(cui.agentMenu());
						break;
					case STATISTICS:
						showStatistics();
						break;
					case DISCONNECT:
						System.out.println("Disconnecting...");
						clusterNode.shutdown();
						clusterNode.getClusterAdministration().disconnectFromGroup(clusterNode.getNodeInfo());
						removeServices();
						System.out.println("Disconnected!");
						firstTime = configure = true;
						break;
					case EXIT:
						System.out.println("Exiting...");
						clusterNode.getClusterAdministration().disconnectFromGroup(clusterNode.getNodeInfo());
						exit = true;
						break;
					default:
						System.err.println("Invalid option");
				}
			}
		} catch (IOException e) {
			LOGGER.error("Error reading input from user");
			e.printStackTrace();
		} catch (NotBoundException e) {
			LOGGER.error("Lookup Error");
		}
		System.out.println("Ending ...");
//		System.out.println("Total transactions: " + Iterables.size(logger.transferItems()));
	}
	
	private void initClusterUI() throws IOException {
		cui = ClusterAdministrationUI.getInstance();
		
		// setups a cluster or connects to a specified one
		cui.configureCluster();
	}
	
	private void initStub() {
		try {
			RemoteEventDispatcher remoteDispatcher;
			// Creates the cluster administration, Cluster Node, the Local Event Dispatcher,
			// the remote Event dispatcher, the agent balancer, and the agent transfer
			
			ClusterAdministration cluster = new ClusterAdministrationImpl();
			
			clusterNode = new ClusterNode(cluster, cui.getClusterNode());
			((ClusterAdministrationImpl)cluster).setClusterNode(clusterNode);
			
			localDispatcher = new DistributedEventDispatcher(clusterNode);
			clusterNode.setLocalEventDispatcher(localDispatcher);
			remoteDispatcher = new RemoteEventDispatcherImpl(clusterNode);
			
			AgentsBalancer agentsBalancer = new AgentsBalancerImpl(clusterNode);
			AgentsTransfer agentsTransfer = new AgentsTransferImpl(clusterNode);
						
			// Sets to the Node, its remote event dispatcher and its agent balancer
			clusterNode.setRemoteEventDispatcher(remoteDispatcher);
			clusterNode.setAgentsBalancer(agentsBalancer);
			clusterNode.setAgentsTransfer(agentsTransfer);
			
			// Bind the remote object's stub in the registry
			if (registry == null) {
				registry = LocateRegistry.createRegistry(clusterNode.getNodeInfo().port());
			}
			
			registry.bind(Node.CLUSTER_COMUNICATION, cluster);
			registry.bind(Node.DISTRIBUTED_EVENT_DISPATCHER, remoteDispatcher);
			registry.bind(Node.AGENTS_BALANCER, agentsBalancer);
			registry.bind(Node.AGENTS_TRANSFER, agentsTransfer);
			
			LOGGER.info("Stub up!");
		} catch (Exception e) {
			LOGGER.error("Error starting stub");
			throw new IllegalStateException();
		}

	}
	
	public void prepareStatistics(StatisticReports distributedStatistics) {
		try {
			registry.bind(Node.STATISTIC_REPORTS, distributedStatistics);
		} catch (Exception e) {
			LOGGER.error("statistics module could not be bound");
		}
	}
	
	private void removeServices() {
		try {
			registry.unbind(Node.CLUSTER_COMUNICATION);
			registry.unbind(Node.DISTRIBUTED_EVENT_DISPATCHER);
			registry.unbind(Node.AGENTS_BALANCER);
			registry.unbind(Node.AGENTS_TRANSFER);
			registry.unbind(Node.STATISTIC_REPORTS);
		} catch (Exception e) {
			LOGGER.error("Error unbinding stub services");
		}
	}
	
	private void prepareLocalAgents() {
		//Resource steel = new Resource("Alloy", "Steel");
		//Resource gold = new Resource("Mineral", "Gold");
		//Resource copper = new Resource("Mineral", "Copper");
		//distributedSimulation.add(new Producer("copper mine", copper, Duration.standardDays(1), 1));
	}
	
	private void addAgentToSimulation(UIAgentChooser achooser) {
		boolean success = false;
		NodeInformation coordinator = null;
		Resource r;
		Agent[] agent = new Agent[achooser.getAmount()];
		String namer = null;
		
		do {
			try {
				
				switch(achooser.getResourceType()) {
					case COPPER:
						r = new Resource("Mineral", "Copper");
						namer = "Copper";
						break;
					case GOLD:
						r = new Resource("Mineral", "Gold");
						namer = "Gold";
						break;
					case STEEL:
					default:
						r = new Resource("Alloy", "Steel");
						namer = "Steel";
						break;
				}
				switch(achooser.getAgentType()) {
					case MARKET: 
						for (int i = 0 ; i < achooser.getAmount() ; i++) {
							agent[i] = new Market(namer+"Market" + random.nextInt(), r);
						}
						LOGGER.info("Adding " + achooser.getAmount() + " Market of " + namer);
						break;
					case CONSUMER:
						for (int i = 0 ; i < achooser.getAmount() ; i++) {
							agent[i] = new Consumer(namer + "Consumer" + random.nextInt(), r, Duration.standardDays(1), 1);
						}
						LOGGER.info("Adding " + achooser.getAmount() + " Consumer of " + namer);
						break;
					case PRODUCER:
					default:
						for (int i = 0 ; i < achooser.getAmount() ; i++) {
							agent[i] = new Producer(namer + "Producer" + random.nextInt(), r, Duration.standardDays(1), 1);
						}
						LOGGER.info("Adding " + achooser.getAmount() + " Producer of " + namer);
						break;
				}
				
				AgentsBalancerImpl agentsBalancerImpl = (AgentsBalancerImpl)clusterNode.getAgentsBalancer();
				coordinator = agentsBalancerImpl.getCoordinator();
				AgentsBalancer remoteBalancer = clusterCommBalance.getStub(coordinator);
				
				for (int i = 0 ; i < achooser.getAmount() ; i++) {
					remoteBalancer.addAgentToCluster(new NodeAgent(clusterNode.getNodeInfo(), agent[i]));
				}
				success = true;
			} catch (NotCoordinatorException e1) {
				success = false;
			} catch (RemoteException e) {
				LOGGER.info("Node down: " + coordinator);
				clusterComm.disconnectNode(coordinator);
				success = false;
			}
		} while(!success);
	}
	
	private CleanableThread setupSimulation() {
		return new CleanableThread("startSim") {
			@Override
			public void run() {
				try {
					distributedSimulation.startAndWait(Duration.standardMinutes(10));
				} catch (InterruptedException e) {
					LOGGER.warn("Simulation interrupted!");
				} catch (IllegalStateException e) {
					LOGGER.warn("Simulation already running");
				}
			}
		};
	}
	
	private void showStatistics() {
		StatisticReports remoteReport;
		NodeStatistics nodeStatistic;
		try {
			for (NodeInformation n : clusterNode.getClusterAdministration().connectedNodes()) {
				try {
					remoteReport = (StatisticReports) clusterComm.getStub(n);
					nodeStatistic = remoteReport.getNodeStatistics();
					
					System.out.println("Node: " + n + " has the following number of agentes: " + nodeStatistic.getNumberOfAgents());
					System.out.println("Node: " + n + " agents status: " + nodeStatistic.getAgentState());
					
				} catch (IllegalStateException e) {
					LOGGER.warn("Node down: " + n);
				} catch (RemoteException e) {
					LOGGER.warn("Node down: " + n);
					clusterComm.disconnectNode(n);
				}
			}
		} catch (RemoteException e) {
			// never happns, local call
		}
	}
}
